/*
 * Copyright (c) 2009, Hamish Morgan. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * 3. Neither the name of the University of Sussex nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */
package locusts.client;

import java.io.IOException;
import locusts.client.renderer.RenderPanel;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import locusts.client.diamondtouch.DTBoundingBoxEvent;
import locusts.client.diamondtouch.DTHandler;
import locusts.client.diamondtouch.DTListener;
import locusts.client.diamondtouch.DTSegmentsEvent;
import locusts.common.ObservableList;
import locusts.common.Player;
import locusts.common.World;
import locusts.common.net.FullUpdateRequest;
import locusts.common.net.InputMessage;
import locusts.common.net.PlayerMessage;
import locusts.common.net.PlayerMessage.NewPlayerRequest;
import locusts.common.net.PlayerMessage.NewPlayerResponse;
import locusts.common.net.PlayerMessage.PlayerDetailsUpdate;
import locusts.common.net.WorldUpdateMessage;
import locusts.common.vibro.VTMessage;
import locusts.common.vibro.VTMessageHandler;
import org.apache.mina.common.IoSession;
import org.apache.mina.handler.demux.MessageHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Class that controls a single client view of the game. 
 * 
 * TODO: there can be multiple players per client.
 *
 *
 * @author Hamish Morgan
 */
public class Client {

    private static final Logger LOG = LoggerFactory.getLogger(Client.class);
    private final RenderPanel panel;
    private final ClientNetHandler net;
    private DTHandler dt;
    private final ClientConfig config;
    private List<ClientConfig> allConfigs;
    /**
     * Hold a list of all players using this client. Since there can be
     * multiple mice and a touch table which can support up to 8 players,
     * there can be an unbounded number of players.
     */
    private HashMap<String, Player> clientPlayers;
    private ObservableList<Player> allPlayers;

    public Client(ClientConfig config) {
        allPlayers = new ObservableList<Player>();
        this.config = config;
        allConfigs = new ArrayList<ClientConfig>();
        allConfigs.add(config);
        clientPlayers = new HashMap<String, Player>();

        panel = new RenderPanel(config, allPlayers);
        panel.start();
        panel.getOvRenderer().setClientConfigs(allConfigs);

        net = new ClientNetHandler(config);

        net.addMessageHandler(WorldUpdateMessage.class,
                new WorldMessageHandler());
        net.addMessageHandler(String.class, new TextHandler());

        net.addMessageHandler(PlayerMessage.class, new PlayerMessageHandler());
        try {
            net.addMessageHandler(VTMessage.class, new VTMessageHandler());
        } catch (Exception e) {
            net.addMessageHandler(VTMessage.class, MessageHandler.NOOP);
            LOG.error(null, e);
        }
        net.addMessageHandler(ClientConfig.class, new ClientConfigHandler());


        net.addMessageHandler(ObservableList.class, new MessageHandler<ObservableList>() {

            public void messageReceived(IoSession session,
                    ObservableList message) throws Exception {
                if (message.isEmpty() || !(message.get(0) instanceof Player))
                    return;

                allPlayers.clear();
                allPlayers.addAll(message);
//                throw new UnsupportedOperationException("Not supported yet.");
            }
        });
        final MouseInput mouseinput = new MouseInput();
        panel.addMouseListener(mouseinput);
        panel.addMouseMotionListener(mouseinput);
        panel.addMouseWheelListener(mouseinput);

        panel.addKeyListener(new KeyAdapter() {

            @Override
            public void keyPressed(KeyEvent e) {
                if (e.getKeyCode() == KeyEvent.VK_U) {
                    net.write(new FullUpdateRequest());
                    LOG.info("Full update requested....");
                }
            }
        });

        if (config.isTouchEnabled()) {
            System.out.println("Starting diamond touch handler...");
            try {
                dt = new DTHandler();
                dt.addListener(new TableInput());
                dt.getTransform().watchComponent(panel);
                dt.start();
            } catch (IOException ex) {
                dt = null;
                LOG.error(null, ex);
            }
        } else {
            dt = null;
        }

    }

    private class ClientConfigHandler implements MessageHandler<ClientConfig> {

        public void messageReceived(IoSession session, ClientConfig message)
                throws Exception {
//            System.out.println("ClientConfig recieved");
            if (config.getId() == message.getId())
                config.set(message);
            else {
                final ListIterator<ClientConfig> it =
                        allConfigs.listIterator();
                boolean found = false;
                while (!found && it.hasNext()) {
                    final ClientConfig cfg = it.next();
                    if (cfg.getId() == message.getId()) {
                        cfg.set(message);
                        found = true;
                    }
                }
                if (!found)
                    allConfigs.add(message);
            }
        }
    }

    public RenderPanel getPanel() {
        return panel;
    }

    /**
     * Handle events produced by a diamond touch table handler (DTHander)
     * class, convert them to game space, and sent them on the network.
     */
    private class TableInput implements DTListener {

        private DTBoundingBoxEvent convertToWorldSpace(DTBoundingBoxEvent e)
                throws NoninvertibleTransformException {
//            final AffineTransform at = panel.keyList.getTransform();
//            at.concatenate(config.getTransform());
            AffineTransform at = config.getTransform();
            ;
//            at.concatenate(panel.keyList.getTransform());
//            at.concatenate(at);
            Point2D origin =
                    new Point2D.Double(e.getXOrigin(), e.getYOrigin());
            Point2D corner =
                    new Point2D.Double(e.getXCorner(), e.getYCorner());
            at.inverseTransform(origin, origin);
            at.inverseTransform(corner, corner);
            return new DTBoundingBoxEvent(e.getSource(), e.getUserId(),
                    e.getTime(), e.getAction(),
                    (int) origin.getX(), (int) origin.getY(),
                    (int) corner.getX(), (int) corner.getY());
        }

        public void segmentsReceived(DTSegmentsEvent msg) {
            LOG.warn("Ignoring DTSegments event.");
        }

        private void sendInputMessage(DTBoundingBoxEvent msg,
                InputMessage.Type type) {
            if (panel.getWorld().getState() == World.State.GAME_OVER)
                return;

            try {
                final String localId = "DT-" + msg.getUserId();
                if (!clientPlayers.containsKey(localId)) {
                    net.write(new NewPlayerRequest(localId));
//                if (!net.containsPlayer(localId)) {
                    // request a new player data.
                    clientPlayers.put(localId, null);
                } else if (clientPlayers.get(localId) == null) {
                    // request was sent but response not yet received
                } else {
                    DTBoundingBoxEvent wmsg = convertToWorldSpace(msg);
                    InputMessage im = new InputMessage.Touch(
                            wmsg.getCenterX(), wmsg.getCenterY(),
                            wmsg.getWidth(), wmsg.getHeight(),
                            clientPlayers.get(localId),
                            type);
                    net.write(im);
                }
            } catch (NoninvertibleTransformException ex) {
                LOG.warn("Failed to send touch event packet.", ex);
            }

        }

        public void boundingBoxDown(DTBoundingBoxEvent msg) {
            sendInputMessage(msg, InputMessage.Type.PRESS);
        }

        public void boundingBoxMoved(DTBoundingBoxEvent msg) {
            sendInputMessage(msg, InputMessage.Type.DRAG);
        }

        public void boundingBoxUp(DTBoundingBoxEvent msg) {
            sendInputMessage(msg, InputMessage.Type.RELEASE);
        }
    }

    /**
     * Handle mouse input, translate messages to game space, and sent them on
     * the network.
     */
    private class MouseInput implements MouseListener, MouseMotionListener,
            MouseWheelListener {

        private MouseEvent convertEventToWorldspace(MouseEvent e) {
            Point2D p = new Point2D.Double(e.getX(), e.getY());
//            AffineTransform at = panel.keyList.getTransform();
//            at.concatenate(config.getTransform());
            AffineTransform at = config.getTransform();

//            at.concatenate(panel.keyList.getTransform());
            try {
                final Point2D q = at.inverseTransform(p, null);
                if (e instanceof MouseWheelEvent) {
                    MouseWheelEvent f = (MouseWheelEvent) e;
                    return new MouseWheelEvent(f.getComponent(), f.getID(), f.
                            getWhen(), f.getModifiers(), (int) q.getX(),
                            (int) q.getY(), f.getClickCount(), f.
                            isPopupTrigger(), f.getScrollType(), f.
                            getScrollAmount(),
                            f.getWheelRotation());
                } else {
                    return new MouseEvent(e.getComponent(), e.getID(), e.
                            getWhen(),
                            e.getModifiers(), (int) q.getX(), (int) q.getY(),
                            e.getClickCount(), e.isPopupTrigger(),
                            e.getButton());
                }
            } catch (NoninvertibleTransformException ex) {
                LOG.error("Failed to convert click to world space.", ex);
                return e;
            }
        }

        public void mousePressed(MouseEvent e) {
            if (panel.getWorld().getState() == World.State.GAME_OVER)
                return;
            final String localId = "Mouse-0";
            if (!clientPlayers.containsKey(localId)) {
                net.write(new NewPlayerRequest(localId));
                clientPlayers.put(localId, null);
            } else if (clientPlayers.get(localId) == null) {
                // request was sent but response not yet received
            } else {
                MouseEvent c = convertEventToWorldspace(e);
                net.write(new InputMessage.Mouse(c.getX(), c.getY(),
                        clientPlayers.get(localId),
                        InputMessage.Type.PRESS));
            }
        }

        public void mouseClicked(MouseEvent e) {
        }

        public void mouseReleased(MouseEvent e) {
            if (panel.getWorld().getState() == World.State.GAME_OVER)
                return;
            final String localId = "Mouse-0";
            if (!clientPlayers.containsKey(localId)) {
                net.write(new NewPlayerRequest(localId));
                clientPlayers.put(localId, null);
            } else if (clientPlayers.get(localId) == null) {
                // request was sent but response not yet received
            } else {
                MouseEvent c = convertEventToWorldspace(e);
                net.write(new InputMessage.Mouse(c.getX(), c.getY(),
                        clientPlayers.get(localId), InputMessage.Type.RELEASE));
            }
        }

        public void mouseEntered(MouseEvent e) {
        }

        public void mouseExited(MouseEvent e) {
        }

        public void mouseDragged(MouseEvent e) {
            if (panel.getWorld().getState() == World.State.GAME_OVER)
                return;
            final String localId = "Mouse-0";
            if (!clientPlayers.containsKey(localId)) {
                net.write(new NewPlayerRequest(localId));
                clientPlayers.put(localId, null);
            } else if (clientPlayers.get(localId) == null) {
                // request was sent but response not yet received
            } else {
                final MouseEvent c = convertEventToWorldspace(e);
                net.write(new InputMessage.Mouse(c.getX(), c.getY(),
                        clientPlayers.get(localId), InputMessage.Type.DRAG));
            }
        }

        public void mouseMoved(MouseEvent e) {
        }

        public void mouseWheelMoved(MouseWheelEvent e) {
        }
    }

    class WorldMessageHandler implements MessageHandler<WorldUpdateMessage> {

        public void messageReceived(IoSession session, WorldUpdateMessage msg)
                throws Exception {
            final World world = panel.getWorld();
            msg.applyTo(world);
        }
    }

    private class PlayerMessageHandler implements
            MessageHandler<PlayerMessage> {

        public void messageReceived(IoSession session, PlayerMessage message)
                throws Exception {
            if (message.getClass().equals(NewPlayerRequest.class)) {
                handleMessage((NewPlayerRequest) message);
            } else if (message.getClass().equals(NewPlayerResponse.class)) {
                handleMessage((NewPlayerResponse) message);
            } else if (message.getClass().equals(PlayerDetailsUpdate.class)) {
                handleMessage((PlayerDetailsUpdate) message);
            }
        }

        public void handleMessage(NewPlayerRequest message) {
            // should not be recieved by a client ever!
        }

        public void handleMessage(NewPlayerResponse message) {
            clientPlayers.put(message.getLocalId(), message.getPlayer());
        }

        public void handleMessage(PlayerDetailsUpdate message) {
            for (Player player : clientPlayers.values()) {
                if (player.getId() == message.getPlayer().getId()) {
                    message.applyTo(player);
                    return;
                } else {
                    // the player info applies to someone else
                }
            }
            LOG.warn("Player not found for update message.");
        }
    }

    private class TextHandler implements MessageHandler<String> {

        public void messageReceived(IoSession session, String message)
                throws Exception {
            System.out.println("Recieved Message From Server:: " + message);
        }
    }
}
